Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / deuxyeux / RTNode-HeltecV4 rns://bfb3becae4b7568796ac5276b6bc5741/deuxyeux/RTNode-HeltecV4


A custom firmware for the Heltec WiFi LoRa 32 V4


>RTNode-HeltecV4 β€” Reticulum Transport Node for Heltec WiFi LoRa 32 V4 (with support for V3)

A custom firmware for the Heltec WiFi LoRa 32 V4 (ESP32-S3 + SX1262) that operates as a Transport Node β€” bridging a local LoRa radio network with a remote TCP/IP backbone (such as rmap.world) over WiFi.

This project was primarily developed with the use of AI assistance.

T282828
Android / Sideband Remote
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Reticulum
β”‚ Sideband │◄── BT ──►│ RNode (BT) β”‚ Backbone
β”‚ App β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ (rnsd /
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ rmap.world)
LoRa Radio β–²
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” WiFi β”‚
◄── RF mesh ──────► β”‚ RTNode-HV4 β”‚ ◄─TCPβ”€β”€β”˜
β”‚ β”‚Transport Nodeβ”‚ β–²
Other RNodes β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”Œβ”€β”€β”€β”΄β”€β”€β”€β”
β”‚ Routerβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜

Built on microReticulum (a C++ port of the Reticulum network stack) and the RNode firmware by Mark Qvist.

Features

β€’ Bidirectional LoRa ↔ TCP bridging β€” local LoRa mesh nodes can reach the global Reticulum backbone and vice versa
β€’ Web-based configuration portal β€” WiFi SSID/password, backbone host/port, LoRa parameters, all configurable via captive portal
β€’ OLED status display β€” real-time status indicators for LoRa, WiFi, WAN (backbone), LAN (local TCP), plus IP address, port, and airtime
β€’ Optional local TCP server β€” serve local devices on your WiFi in addition to the backbone connection
β€’ Automatic reconnection β€” WiFi and TCP connections recover from drops with exponential backoff
β€’ ESP32 memory-optimized β€” table sizes, timeouts, and caching tuned for the constrained MCU environment
β€’ Dual board support β€” supports both Heltec V3 (8MB flash) and V4 (16MB flash, 2MB PSRAM) with automatic board and PSRAM detection

Hardware

This firmware was designed for the Heltec WiFi LoRa 32 V4. This board was chosen for its 2MB PSRAM and LoRa capabilities. While the V3 is supported, it uses the ESP32-S3FN8 which has no PSRAM. The firmware detects PSRAM at runtime and allocates the TLSF memory pool from SPIRAM when available, falling back to internal SRAM (~170 KB) on boards without PSRAM.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Component β”‚ Heltec V3 β”‚ Heltec V4 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ MCU β”‚ ESP32-S3 (ESP32-S3FN8) β”‚ ESP32-S3 (ESP32-S3FH4R2) β”‚
β”‚ Flash β”‚ 8 MB β”‚ 16 MB β”‚
β”‚ PSRAM β”‚ None β”‚ 2 MB (QSPI) β”‚
β”‚ Radio β”‚ SX1262 β”‚ SX1262 + GC1109 PA β”‚
β”‚ TX Power β”‚ Up to 22 dBm β”‚ Up to 28 dBm β”‚
β”‚ Display β”‚ SSD1306 OLED 128Γ—64 β”‚ SSD1306 OLED 128Γ—64 β”‚
β”‚ WiFi β”‚ 2.4 GHz 802.11 b/g/n β”‚ 2.4 GHz 802.11 b/g/n β”‚
β”‚ USB β”‚ Native USB CDC β”‚ Native USB CDC β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quick Start

Option A: Easy Flash (no PlatformIO required)

The easiest way to flash a pre-built firmware. You only need Python 3 and a USB cable.

T282828
T8b949e# Clone this repo (or download just flash.py + the firmware binary)
git clone https://github.com/jrl290/RTNode-HeltecV4.git
Tffa657cd RTNode-HeltecV4

T8b949e# Download latest firmware from GitHub Releases and flash
T8b949e# (auto-detects V3 vs V4 from flash size)
python flash.py

T8b949e# Optional: use your machine's installed esptool instead of the bundled copy
python flash.py --use-system-esptool

T8b949e# Or specify board explicitly
python flash.py --board v3
python flash.py --board v4

T8b949e# Or flash a local binary
python flash.py --file rtnode_heltec_v4.bin


By default, T383838flash.py uses the bundled T383838Release/esptool/esptool.py for reproducible flashing. Only use T383838--use-system-esptool if you explicitly want to override that with a host-installed esptool.

The flash utility auto-detects whether a V3 or V4 is connected by querying the flash size (8MB = V3, 16MB = V4). You can override with T383838--board v3 or T383838--board v4. It will list all available serial ports and prompt you to choose one. If no ports are detected, you may need to hold the BOOT button while pressing RESET to enter download mode.

Option B: Build from Source (PlatformIO)

For development or customization:

T282828
T8b949e# Prerequisites: PlatformIO installed (VS Code extension or CLI)

git clone https://github.com/jrl290/RTNode-HeltecV4.git
Tffa657cd RTNode-HeltecV4

T8b949e# Build for V4
pio run -e heltec_V4_boundary

T8b949e# Build for V3
pio run -e heltec_V3_boundary

T8b949e# Flash (via PlatformIO)
pio run -e heltec_V4_boundary -t upload

T8b949e# Or create a merged binary and flash with the utility
python flash.py --merge-only T8b949e# creates merged firmware bin
python flash.py T8b949e# flash it (auto-detects board)

T8b949e# Monitor serial output (optional)
pio device monitor -e heltec_V4_boundary


Option C: Manual esptool Flash

If you have the merged binary (T383838rtnode_heltec_v4.bin), you can flash it with a single esptool command:

T282828
esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud T79c0ff921600 Tffea00\
write_flash -z --flash_mode qio --flash_freq 80m --flash_size 16MB Tffea00\
0x0 rtnode_heltec_v4.bin


Replace T383838/dev/ttyACM0 with your serial port (T383838/dev/cu.usbmodem* on macOS, T383838COM3 on Windows).

On first boot (or if no configuration is found), the device automatically enters the Configuration Portal.

Configuration Portal

Entering Config Mode

The config portal activates automatically on:
β€’ First boot β€” when no saved configuration exists
β€’ Button hold >5 seconds β€” hold the PRG button for 5+ seconds, the device reboots into config mode

When active, the device creates a WiFi access point named T383838RNode-Boundary-Setup (open network). A captive portal should appear automatically when you connect; if not, browse to T383838http://192.168.4.1.

Config Page Options

The web form has four sections:

πŸ“Ά WiFi Network
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Field β”‚ Description β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ WiFi β”‚ Enable/Disable (disable for LoRa-only repeater mode) β”‚
β”‚ SSID β”‚ Your WiFi network name β”‚
β”‚ Password β”‚ WiFi password β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🌐 TCP Backbone
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Field β”‚ Description β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Mode β”‚ T383838Disabled or T383838Client (connect to backbone) β”‚
β”‚ Backbone Host β”‚ IP address or hostname of backbone server (e.g. T383838rmap.world) β”‚
β”‚ Backbone Port β”‚ TCP port (default: T3838384242) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“‘ Local TCP Server (optional)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Field β”‚ Description β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Local TCP Server β”‚ Enable/Disable β€” runs a TCP server on your WiFi for local Reticulum nodes to… β”‚
β”‚ TCP Port β”‚ Port to listen on (default: T3838384242) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“» LoRa Radio
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Field β”‚ Description β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Frequency β”‚ e.g. T383838867.200 MHz β€” must match your other RNodes β”‚
β”‚ Bandwidth β”‚ 7.8 kHz – 500 kHz (typically T383838125 kHz) β”‚
β”‚ Spreading Factor β”‚ SF6 – SF12 (typically T383838SF7 for backbone, T383838SF10 for long range) β”‚
β”‚ Coding Rate β”‚ 4/5 – 4/8 β”‚
β”‚ TX Power β”‚ 2 – 28 dBm β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After saving, the device reboots with the new configuration applied.

OLED Display Layout

The 128Γ—64 OLED is split into two panels:

Left Panel β€” Status Indicators (64Γ—64)

T282828
● LORA ← filled circle = radio online
β—‹ wifi ← unfilled circle = WiFi disconnected
● WAN ← filled = backbone TCP connected
● LAN ← filled = local TCP client connected
────────────────
Air:0.3% ← current LoRa airtime
β–“β–“β–“β–“β–“ ||||||| ← battery, signal quality

β€’ Filled circle (●) = active/connected
β€’ Unfilled circle (β—‹) = inactive/disconnected
β€’ Labels are UPPERCASE when active, lowercase when inactive (except LAN which is always uppercase)
β€’ LAN row is hidden when the Local TCP Server is disabled in configuration β€” the remaining layout stays in place

Right Panel β€” Device Info (64Γ—64)

T282828
β–“β–“ RTNode-HV4 β–“β–“ ← title bar (inverted)
867.200MHz ← LoRa frequency
SF7 125k ← spreading factor & bandwidth
──────────────── ← separator
192.168.1.42 ← WiFi IP address (or "No WiFi")
Port:4242 ← Local TCP server port
──────────────── ← separator

β€’ Port shows the Local TCP server port (the port local nodes connect to), not the backbone port
β€’ Port line is hidden when the Local TCP Server is disabled

Interface Modes

The firmware runs up to three RNS interfaces simultaneously, using different interface modes to control announce propagation and routing behavior:

LoRa Interface β€” T383838MODE_ACCESS_POINT

The LoRa radio operates in Access Point mode. In Reticulum, this means:
β€’ The interface broadcasts its own announces but blocks rebroadcast of remote announces from crossing to LoRa
β€’ This prevents backbone announces (hundreds of remote destinations) from flooding the limited-bandwidth LoRa channel
β€’ Local nodes discover the transport node directly; the transport node answers path requests for remote destinations from its cache

TCP Backbone Interface β€” T383838MODE_BOUNDARY

The TCP backbone connection uses T383838MODE_BOUNDARY (T3838380x20), a custom transport mode adapted for the memory-constrained ESP32 environment. In this mode:
β€’ Incoming announces from the backbone are received and cached, but not stored in the path table by default β€” only stored when specifically requested via a path request from a local LoRa node
β€’ This prevents the path table (limited to 48 entries on ESP32) from being overwhelmed by thousands of backbone destinations
β€’ When the path table needs to be culled, backbone-learned paths are evicted first, preserving locally-needed LoRa paths

Optional Local TCP Server β€” T383838MODE_ACCESS_POINT

If enabled, a TCP server on the WiFi network allows local Reticulum nodes to connect. It also uses Access Point mode, with the same announce filtering as LoRa.

Implementation details:
β€’ Each TCP interface must have a unique name to produce a unique interface hash β€” the backbone uses T383838"TcpInterface" and the local server uses T383838"LocalTcpInterface". Without distinct names, both interfaces produce the same hash, causing the interface map lookup to fail when routing packets.
β€’ TCP interfaces are configured with a 10 Mbps bitrate, which causes Reticulum's Transport to prefer TCP paths over LoRa paths (typically ~1–10 kbps) when both are available for the same destination.
β€’ When the Local TCP Server is disabled, its status indicator (LAN) and port number are hidden from the OLED display.

Routing & Memory Customizations

The ESP32-S3 has limited RAM compared to a desktop Reticulum node. Several customizations were made to the microReticulum library to operate reliably within these constraints:

Table Size Limits

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Table β”‚ Default (Desktop) β”‚ RTNode-HeltecV4 β”‚ Rationale β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Path table (T383838_destination_table) β”‚ Unbounded β”‚ 48 entries β”‚ Prevents unbounded gro… β”‚
β”‚ Hash list (T383838_hashlist) β”‚ 1,000,000 β”‚ 32 β”‚ Packet dedup list; sma… β”‚
β”‚ Path request tags (T383838_max_pr_tags) β”‚ 32,000 β”‚ 32 β”‚ Pending path requests … β”‚
β”‚ Known destinations β”‚ 100 β”‚ 24 β”‚ Identity cache; rarely… β”‚
β”‚ Max queued announces β”‚ 16 β”‚ 4 β”‚ Outbound announce queu… β”‚
β”‚ Max receipts β”‚ 1,024 β”‚ 20 β”‚ Packet receipt tracking β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Timeout Reductions

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Setting β”‚ Default β”‚ RTNode-HeltecV4 β”‚ Rationale β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Destination timeout β”‚ 7 days β”‚ 1 day β”‚ Free memory faster; stale paths re… β”‚
β”‚ Pathfinder expiry β”‚ 7 days β”‚ 1 day β”‚ Same as above β”‚
β”‚ AP path time β”‚ 24 hours β”‚ 6 hours β”‚ AP paths go stale faster in mesh e… β”‚
β”‚ Roaming path time β”‚ 6 hours β”‚ 1 hour β”‚ Mobile nodes change paths frequent… β”‚
β”‚ Table cull interval β”‚ 5 seconds β”‚ 60 seconds β”‚ Less CPU overhead on culling β”‚
β”‚ Job/Clean/Persist intervals β”‚ 5m/15m/12h β”‚ 60s/60s/60s β”‚ More frequent housekeeping for MCU… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Selective Backbone Caching

The most critical optimization: backbone announces are not stored in the path table by default. A backbone like T383838rmap.world may advertise hundreds of destinations. Storing them all would evict every local LoRa path.

Instead:
1. Backbone announces are received and their packets cached to flash storage
2. When a local LoRa node requests a path, the transport node checks its cache and responds directly
3. Only specifically requested paths get a path table entry
4. Path table culling prioritizes evicting backbone entries over local ones

Default Route Forwarding

When a transport-addressed packet arrives from LoRa but the transport node has no path table entry for it, the firmware:
1. Strips the transport headers (converts T383838HEADER_2 β†’ T383838HEADER_1/BROADCAST)
2. Forwards the raw packet to the backbone interface
3. Creates reverse-table entries so proofs can route back to the sender

This acts as a default route β€” any packet the transport node can't route locally gets forwarded to the backbone.

Cached Packet Unpacking Fix

The original microReticulum T383838get_cached_packet() function called T383838update_hash() after deserializing cached packets from flash. However, T383838update_hash() only computes the packet hash β€” it does not parse the raw bytes into fields like T383838destination_hash, T383838data, T383838flags, etc.

This was changed to call T383838unpack() instead, which parses all packet fields AND computes the hash. Without this fix, path responses contained empty destination hashes and were silently dropped by LoRa nodes.

β”‚ Note: T383838unpack() only parses the plaintext routing envelope (destination hash, flags, hops,
β”‚ transport headers). It does not decrypt the end-to-end encrypted payload. Every Reticulum
β”‚ transport node performs equivalent header parsing during normal routing β€” this is standard
β”‚ behavior, not a security concern.

Path Table Update Fix

The C++ T383838std::map::insert() method silently does nothing when a key already exists β€” unlike Python's T383838dict[key] = value which replaces. The original microReticulum code used T383838insert() to update path table entries, meaning stale LoRa paths were never replaced by newer TCP paths (or vice versa).

This was fixed by calling T383838erase() before T383838insert(), ensuring updated path entries always replace stale ones. Without this fix, the transport node would continue routing packets via an old interface even after a better path was learned.

Interface Name Uniqueness

Each RNS interface must have a unique name because the name is hashed to produce the interface identifier used in path table lookups. If two interfaces share the same name, they produce the same hash, and T383838std::map can only store one β€” causing the Transport layer to fail to resolve the correct outbound interface for packets.

The TcpInterface constructor accepts an explicit T383838name parameter: the backbone uses T383838"TcpInterface" and the local server uses T383838"LocalTcpInterface".

Connecting to the Backbone

Example: Connect to rmap.world

In the configuration portal:
1. Set WiFi SSID and password
2. Set TCP Backbone Mode to Client
3. Set Backbone Host to T383838rmap.world
4. Set Backbone Port to T3838384242
5. Save and reboot

Example: Local rnsd Server

On your server, configure T383838rnsd with a TCP Server Interface in T383838~/.reticulum/config:

T282828
Tff7b72[interfaces]
Tff7b72[[TCP Server Interface]]
Te6edf3type Tff7b72= Ta5d6ffTCPServerInterface
Te6edf3listen_host Tff7b72= Ta5d6ff0.0.0.0
Te6edf3listen_port Tff7b72= Ta5d6ff4242


Then configure the transport node as a Client pointing to your server's IP.

Example: rnsd Connects to Transport Node

On your server, configure T383838rnsd with a TCP Client Interface:

T282828
Tff7b72[interfaces]
Tff7b72[[TCP Client to Transport Node]]
Te6edf3type Tff7b72= Ta5d6ffTCPClientInterface
Te6edf3target_host Tff7b72= Ta5d6ff<transport-node-ip>
Te6edf3target_port Tff7b72= Ta5d6ff4242


Set the transport node's Local TCP Server to Enabled (port 4242).

Architecture

Key Files

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ File β”‚ Purpose β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838RNode_Firmware.ino β”‚ Main firmware β€” transport mode initialization, interface setup, button han… β”‚
β”‚ T383838BoundaryMode.h β”‚ Transport node state struct, EEPROM load/save, configuration defaults β”‚
β”‚ T383838BoundaryConfig.h β”‚ Web-based captive portal for configuration β”‚
β”‚ T383838TcpInterface.h β”‚ TCP interface for both backbone and local server (implements T383838RNS::Interfac… β”‚
β”‚ T383838Display.h β”‚ OLED display layout β€” transport node status page β”‚
β”‚ T383838flash.py β”‚ Flash utility β€” list serial ports, download from GitHub, merge & flash fir… β”‚
β”‚ T383838Boards.h β”‚ Board variant definitions for V3 and V4 β”‚
β”‚ T383838platformio.ini β”‚ Build targets: T383838heltec_V3_boundary, T383838heltec_V4_boundary, and T383838heltec_V4_bound… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Library Patches

The firmware depends on microReticulum T3838380.2.4, automatically fetched by PlatformIO on first build. After the first build, the library sources under T383838.pio/libdeps/heltec_V4_boundary/microReticulum/src/ need the patches described in "Routing & Memory Customizations" above. Key files modified:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ File β”‚ Changes β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838Transport.cpp β”‚ Selective caching, default route forwarding, transport-aware culling, T383838get_cache… β”‚
β”‚ T383838Transport.h β”‚ T383838MODE_BOUNDARY, T383838PacketEntry, T383838Callbacks, T383838cull_path_table(), configurable table si… β”‚
β”‚ T383838Identity.cpp β”‚ T383838_known_destinations_maxsize = 24, T383838cull_known_destinations() β”‚
β”‚ T383838Type.h β”‚ T383838MODE_BOUNDARY = 0x20, reduced T383838MAX_QUEUED_ANNOUNCES, T383838MAX_RECEIPTS, shorter timeo… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Memory Usage (typical, V4)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Resource β”‚ Used β”‚ Available β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ RAM β”‚ ~21.7% β”‚ 320 KB β”‚
β”‚ Flash β”‚ ~18.4% β”‚ 16 MB β”‚
β”‚ PSRAM β”‚ Dynamic β”‚ 2 MB β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

License

This project is licensed under the GNU General Public License v3.0 β€” see LICENSE for details.

Based on:
β€’ RNode Firmware by Mark Qvist (GPL-3.0)
β€’ microReticulum by Chris Attermann (GPL-3.0)
β€’ Reticulum by Mark Qvist (MIT)


Served by rngit 1.4.2 - Generated in 0.51s